Retropikzel's website - Blog - 2024-06-21 - r7rs-pffi libcurl example

UPDATE: This post is about old version of r7rs-pffi which did not support callbacks. Callback support is now beign/already added and the code in the repository is updated to reflect this.

This post is related to my previous blog post Portable foreign function interface for R7RS small.

I thought I'd write a small example of how to use the library. In my previous post I tried to explain more about why the library exists in the first place. This time we are getting our hands dirty, I'll make this more like a tutorial.

Prerequisites

If you are on guix just run

guix shell guile curl

Scheme implementation

If you want to follow along you need to have r7rs-pffi supported Scheme implementation installed. At the time of writing these are:

I'll be instructing how to run the examples with each implementation, so it does not matter which one you choose. The code will be exactly same on all of them.

If you are not on linux, Sagittarius, Racket and Kawa also run on other platforms like Windows. For Kawa we are using the kawa.jar, which is inside the "lib" directory of the precompiled zip in here.

r7rs-pffi

Download the latest release from https://codeberg.org/r7rs-pffi/pffi/releases and move the directory named "retropikzel" into your projects root directory.

libcurl

Unix

On unix systems you need libcurl installed, on Debian based linuxes it can be installed with

apt install libcurl4

Windows

Download curl from https://curl.se/windows/, unzip it and move the bin/libcurl-x64.dll to your projects root directory with name "curl.dll". I used the 64 bit version.

The code

In your projects root directory create file called main.scm and add the following code snippets to it.

Import scheme base and r7rs-pffi:

(import (scheme base)
        (scheme write)
        (retropikzel pffi v0-2-2 main))

Load libcurl. We define the current directory "." as additional load path because I forgot to add it to pffi-shared-object-auto-load load paths. Many paths are there already so usually no path is needed. And in this case Sagittarius automatically on windows adds the "." path but racket does not.

The headers are for implementations that compile to C. Prefix "lib" is added to library name on unixes automatically so no need to have it here. With ".4" as additional versions on linux these filenames are looked for (from varied system directories):

on windows the library searches for

(define libcurl (pffi-shared-object-auto-load (list "curl/curl.h") ; Headers
                                          (list ".") ; Additional search paths
                                          "curl" ; The named of shared object without the lib prefix
                                          (list ".4"))) ;Additional versions to search

Define the foreign procedures.

The arguments for pffi-define are

(pffi-define curl-easy-init libcurl 'curl_easy_init 'pointer (list))
(pffi-define curl-easy-setopt libcurl 'curl_easy_setopt 'int (list 'pointer 'int 'pointer))
(pffi-define curl-easy-perform libcurl 'curl_easy_perform 'int (list 'pointer))

Define some variables that are defined in C header files.

I had to make a small C file and compile it to print these out. Sometimes you can find the values straight from headers. This is a little bit cumbersome but there is a solution of which I will write in the future:

(define CURLOPT-FOLLOWLOCATION 52)
(define CURLOPT-URL 10002)

Init curl and define our favorite websites url. We also make the pointer of the url string.

(define handle (curl-easy-init))
(define url (pffi-string->pointer "https://scheme.org"))

Tell curl to follow redirects. We will use the url pointer as argument, which is not 100% correct but works for us for now.

(define curl-code1 (curl-easy-setopt handle CURLOPT-FOLLOWLOCATION url))

Set the url.

(define curl-code2 (curl-easy-setopt handle CURLOPT-URL url))

Display the curl-codes for debugging. See libcurl error codes.

(display curl-code1)
(newline)
(display curl-code2)
(newline)

Perform the GET to our url and store the result code.

On C code you would pass a callback function to read the content of the webpage returned by curls request. Unfortunately r7rs-pffi does not support callback functions, it might some day but I can not make any promises, so the webpage html code will be outputted to stdout by curl.

(define result (curl-easy-perform handle))

Show the result code for debugging.

(display result)
(newline)

And here is the full code.

(import (scheme base)
        (scheme write)
        (retropikzel pffi v0-2-2 main))

(define libcurl (pffi-shared-object-auto-load (list "curl/curl.h") ; Headers
                                          (list ".") ; Additional search paths
                                          "curl" ; The named of shared object without the lib prefix
                                          (list ".4"))) ;Additional versions to search

(pffi-define curl-easy-init libcurl 'curl_easy_init 'pointer (list))
(pffi-define curl-easy-setopt libcurl 'curl_easy_setopt 'int (list 'pointer 'int 'pointer))
(pffi-define curl-easy-perform libcurl 'curl_easy_perform 'int (list 'pointer))
(define CURLOPT-FOLLOWLOCATION 52)
(define CURLOPT-URL 10002)

(define handle (curl-easy-init))
(define url (pffi-string->pointer "https://scheme.org"))

(define curl-code1 (curl-easy-setopt handle CURLOPT-FOLLOWLOCATION url))

(define curl-code2 (curl-easy-setopt handle CURLOPT-URL url))

(display curl-code1)
(newline)
(display curl-code2)
(newline)

(display result)
(newline)

Running the code

Sagittarius

sash -L . main.scm

on windows use sash.exe, or most propably you need the full path of the exe.

Guile

guile -L . main.scm

Kawa

Java version 21 is used, the new java FFI might be little different on 22 so it might not work.

java --add-exports java.base/jdk.internal.foreign.abi=ALL-UNNAMED --add-exports java.base/jdk.internal.foreign.layout=ALL-UNNAMED --add-exports java.base/jdk.internal.foreign=ALL-UNNAMED --enable-native-access=ALL-UNNAMED --enable-preview -jar kawa.jar main.scm

on widows use java.exe

Racket

You need to have r7rs installed with:

raco pkg install r7rs

and then run:

racket -I r7rs --make -S . --script main.scm

Chicken

Install r7rs with:

chicken-install r7rs

and install libcurl, on debian and derivates:

apt install libcurl4-openssl-dev

copy the pffi files to projects root directory like this:

cp retropikzel/pffi/v0-2-2/main.sld retropikzel.pffi.v0-2-2.main.scm
cp retropikzel/pffi/v0-2-2/chicken.scm retropikzel.pffi.v0-2-2.chicken.scm

compile them:

csc -X r7rs -R r7rs -sJ retropikzel.pffi.v0-2-2.chicken.scm
csc -X r7rs -R r7rs -sJ retropikzel.pffi.v0-2-2.main.scm

and then compile the main program:

csc -X r7rs -R r7rs -L -lcurl main.scm

and then run it:

./main

Closing words

And thats how you use the r7rs-pffi to make same code work on multiple implementations. I'm working on a blog post about a tool to automatically generate bindings to C libraries to make this easier.

The whole code for this can be found in https://codeberg.org/r7rs-pffi/example-libcurl.